package jp.ddo.trismegistos.dao.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jp.ddo.trismegistos.dao.DaoManager;
import jp.ddo.trismegistos.exception.RsException;

import org.apache.log4j.Logger;

/**
 * @author Nagai Satoshi
 */
abstract public class BaseDao extends AbstractBaseDao {

	/** ログ */
	private static final Logger log = Logger.getLogger(BaseDao.class.getName());

	public BaseDao(final DaoManager manager) {
		super(manager);
	}

	protected <T> List<T> executeSelect(final String sqlId,
			final Map<String, Object> params, final Class<T> resultClass)
			throws RsException {
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		List<T> result = null;
		try {
			conn = getConnection(false);
			ps = getPreparedStatement(conn, getSql(sqlId), params);
			rs = ps.executeQuery();
			result = processResult(rs, resultClass);
			conn.commit();
		} catch (final SQLException e) {
			rollBackConnection(conn);
			throw new RsException(e);
		} catch (final RsException e) {
			rollBackConnection(conn);
			throw e;
		} finally {
			closeResultSet(rs);
			closeStatement(ps);
			closeConnection(conn);
		}
		return result;
	}

	public int executeInsert(final String sqlId,
			final Map<String, Object> params) {
		return executeUpdate(sqlId, params);
	}

	public int executeUpdate(final String sqlId,
			final Map<String, Object> params) {
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		int result = 0;
		try {
			conn = getConnection(false);
			ps = getPreparedStatement(conn, getSql(sqlId), params);
			result = ps.executeUpdate();
			conn.commit();
		} catch (final SQLException e) {
			rollBackConnection(conn);
			throw new RsException(e);
		} finally {
			closeResultSet(rs);
			closeStatement(ps);
			closeConnection(conn);
		}
		return result;
	}

	public int executeDelete(final String sqlId,
			final Map<String, Object> params) {
		return executeUpdate(sqlId, params);
	}

	protected PreparedStatement getPreparedStatement(final Connection conn,
			final String sql, final Map<String, Object> params)
			throws SQLException {
		final Map<Integer, String> paramMapping = getParamMapping(sql);
		final String sqlForPreparedStatement = getSqlForPreparedStatement(sql,
				paramMapping);
		final PreparedStatement ps = conn
				.prepareStatement(sqlForPreparedStatement);
		setParams(ps, params, paramMapping);
		return ps;
	}

	private Map<Integer, String> getParamMapping(final String sql) {
		final Map<Integer, String> mapping = new HashMap<Integer, String>();
		final int len = sql.length();
		for (int begin = 0, count = 1, left = 0, right = 0; begin < len; begin = right + 1, count++) {
			left = sql.indexOf('#', begin);
			if (left < 0) {
				break;
			}
			right = sql.indexOf('#', left + 1);
			if (right < 0) {
				throw new RsException("placeHolder is not end : " + sql);
			}
			mapping.put(count, sql.substring(left + 1, right));
		}
		return mapping;
	}

	private String getSqlForPreparedStatement(final String sql,
			final Map<Integer, String> paramMapping) {
		String result = sql;
		for (final Map.Entry<Integer, String> entry : paramMapping.entrySet()) {
			result = result.replaceAll("#" + entry.getValue() + "#", "?");
		}
		return result;
	}

	private void setParams(final PreparedStatement ps,
			final Map<String, Object> params,
			final Map<Integer, String> paramMapping) throws SQLException {
		// バインドキーと値のマップを回す
		for (final Map.Entry<String, Object> entry : params.entrySet()) {
			final String key = entry.getKey();
			final Object value = entry.getValue();
			// バインド先が複数の可能性があるので、バインド先インデックスのSetを作成する
			final Set<Integer> idxSet = new HashSet<Integer>();
			// バインド先インデックスとバインドキーのマップを回す
			for (final Map.Entry<Integer, String> mapping : paramMapping
					.entrySet()) {
				if (mapping.getValue().equals(key)) {
					idxSet.add(mapping.getKey());
				}
			}
			if (idxSet.isEmpty()) {
				// TODO バインド先がない場合はWarnでスキップでよい？
				log.warn("There is no key to bind. Key : " + key);
			}
			for (final Integer idx : idxSet) {
				setValue(ps, idx, value);
			}
		}
	}
}
